{T}

性能设计篇之"秒杀" [2026重制版]

核心变更说明:本文基于原本文档第61篇重写,全面更新至2026年技术栈。新增完整的高并发秒杀架构设计、CDN 边缘计算削峰方案、Redis 预扣库存 + 异步下单、防刷限流完整实现、多级缓存预热策略、性能压测数据和生产级代码示例。


一、问题背景:秒杀系统的技术挑战

1.1 什么是秒杀?

秒杀(Flash Sale) 是一种限时限量促销方式——以极低价格销售有限数量的商品,在短时间内吸引大量用户抢购。

图表渲染中…

1.2 核心挑战分析

挑战维度问题描述严重程度
瞬时高并发百万用户同时请求⭐⭐⭐⭐⭐ 致命
超卖问题库存扣减不一致⭐⭐⭐⭐⭐ 致命
热点数据所有请求打同一行记录⭐⭐⭐⭐⭐ 致命
恶意刷单脚本/机器人抢购⭐⭐⭐⭐ 严重
系统雪崩依赖服务被拖垮⭐⭐⭐⭐ 严重
用户体验页面加载慢/按钮无响应⭐⭐⭐ 一般

1.3 为什么秒杀如此困难?

图表渲染中…

核心矛盾:百万级 QPS 的请求 vs 单行数据库记录的写入瓶颈。


二、秒杀系统架构设计

2.1 整体架构图

图表渲染中…

2.2 各层职责详解

层级组件核心职责技术手段
L1: CDNCloudflare/Akamai静态资源、JS/CSS/图片全球节点就近访问
L2: 网关Kong/APISIXIP 限流、黑名单、WAF全局限流、人机验证
L3: 接入Go/Rust 高性能服务请求校验、资格检查协程/goroutine
L4: 缓存Redis Cluster库存预扣、去重、计数Lua 脚本原子操作
L5: 业务微服务集群订单创建、支付、通知异步消息驱动
L6: 存储MySQL + ES数据持久化、搜索分库分表

三、核心模块实现

3.1 活动预热 — 缓存预热

在秒杀开始前,将商品信息、库存等数据提前加载到 Redis:

java
@Service
@RequiredArgsConstructor
@Slf4j
public class SeckillWarmUpService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final ProductMapper productMapper;

    /** 秒杀活动 Key 前缀 */
    private static final String SECKILL_STOCK_KEY = "seckill:stock:";
    private static final String SECKILL_USER_SET = "seckill:users:";  // 已购买用户 Set
    private static final String SECKILL_ACTIVITY = "seckill:activity:";

    /**
     * 预热秒杀活动数据
     * 在活动开始前 10 分钟执行
     */
    @Scheduled(cron = "0 */5 * * * ?")  // 每5分钟检查一次
    public void warmUpActivities() {
        List<SeckillActivity> activities = activityMapper.selectUpcomingActivities(
            LocalDateTime.now(),
            LocalDateTime.now().plusMinutes(15)
        );

        for (SeckillActivity activity : activities) {
            warmUpActivity(activity);
        }
    }

    /**
     * 预热单个活动
     */
    public void warmUpActivity(SeckillActivity activity) {
        String activityKey = SECKILL_ACTIVITY + activity.getId();
        String stockKey = SECKILL_STOCK_KEY + activity.getProductId();

        // 1. 设置活动状态(未开始)
        redisTemplate.opsForValue().set(
            activityKey,
            SeckillActivityStatus.WAITING.name(),
            Duration.ofMinutes(30)
        );

        // 2. 预加载库存到 Redis(使用 Redis Hash)
        Map<String, String> stockData = new HashMap<>();
        stockData.put("total", String.valueOf(activity.getTotalStock()));
        stockData.put("remaining", String.valueOf(activity.getTotalStock()));
        stockData.put("version", "0");  // 乐观锁版本号

        redisTemplate.opsForHash().putAll(stockKey, stockData);

        // 3. 设置过期保护(防止忘记清理)
        redisTemplate.expire(stockKey, Duration.ofHours(24));

        // 4. 初始化已购买用户集合(用于去重)
        redisTemplate.delete(SECKILL_USER_SET + activity.getId());

        log.info("秒杀活动预热完成: activityId={}, productId={}, stock={}",
            activity.getId(), activity.getProductId(), activity.getTotalStock());
    }
}

3.2 秒杀核心接口 — Redis 预扣库存

java
@RestController
@RequestMapping("/api/seckill")
@RequiredArgsConstructor
@Slf4j
public class SeckillController {

    private final SeckillService seckillService;
    private final RateLimiter rateLimiter;  // Guava 令牌桶限流

    /**
     * 秒杀下单接口
     *
     * 完整流程:
     * 1. 用户资格校验(是否登录、是否已购买)
     * 2. Redis 预扣库存(Lua 脚本原子操作)
     * 3. 发送异步订单消息
     * 4. 立即返回"排队中"给用户
     */
    @PostMapping("/{activityId}/order")
    public ResponseEntity<SeckillResult> seckill(
            @PathVariable Long activityId,
            @RequestHeader("X-User-ID") Long userId,
            @RequestHeader("X-Token") String token) {

        // ====== L3: 快速失败校验 ======

        // 1. 限流:单用户每秒最多 1 次请求
        if (!rateLimiter.tryAcquire()) {
            return ResponseEntity.ok(SeckillResult.fail("操作过于频繁,请稍后再试"));
        }

        // 2. Token 校验(快速拒绝无效请求)
        if (!tokenService.validateToken(userId, token)) {
            return ResponseEntity.status(401).body(SeckillResult.fail("身份验证失败"));
        }

        // ====== L4: Redis 原子预扣库存 ======
        SeckillResult result = seckillService.preDeductStock(userId, activityId);

        return ResponseEntity.ok(result);
    }
}
java
@Service
@RequiredArgsConstructor
@Slf4j
public class SeckillService {

    private final StringRedisTemplate stringRedisTemplate;
    private final KafkaTemplate<String, Object> kafkaTemplate;
    private final RedissonClient redissonClient;

    private static final String STOCK_SCRIPT =
        "local stockKey = KEYS[1] " +
        "local userSetKey = KEYS[2] " +
        "local userId = ARGV[1] " +
        "local activityId = ARGV[2] " +
        "" +
        "-- 1. 检查是否重复购买 " +
        "if redis.call('SISMEMBER', userSetKey, userId) == 1 then " +
        "    return {0, 'repeat_purchase'} " +
        "end " +
        "" +
        "-- 2. 使用 Watch + CAS 扣减库存 " +
        "local current = tonumber(redis.call('HGET', stockKey, 'remaining')) " +
        "if current == nil or current <= 0 then " +
        "    return {0, 'sold_out'} " +
        "end " +
        "" +
        "local version = tonumber(redis.call('HGET', stockKey, 'version')) " +
        "redis.call('HSET', stockKey, 'version', version + 1) " +
        "redis.call('HSET', stockKey, 'remaining', current - 1) " +
        "redis.call('SADD', userSetKey, userId) " +
        "" +
        "return {1, 'success', current - 1}";

    /**
     * Redis 原子预扣库存(Lua 脚本保证原子性)
     */
    @Transactional
    public SeckillResult preDeductStock(Long userId, Long activityId) {
        String stockKey = "seckill:stock:" + activityId;
        String userSetKey = "seckill:users:" + activityId;

        try {
            // 执行 Lua 脚本 — 原子性保证
            DefaultRedisScript<List> script = new DefaultRedisScript<>();
            script.setScriptText(STOCK_SCRIPT);
            script.setResultType(List.class);

            List<Object> result = stringRedisTemplate.execute(
                script,
                Arrays.asList(stockKey, userSetKey),
                String.valueOf(userId),
                String.valueOf(activityId)
            );

            long code = (Long) result.get(0);
            String message = (String) result.get(1);

            if (code == 1) {
                // ====== 预扣成功 → 发送异步订单消息 ======
                int remainingStock = ((Number) result.get(2)).intValue();

                OrderCreatedEvent event = OrderCreatedEvent.builder()
                    .userId(userId)
                    .activityId(activityId)
                    .status(OrderStatus.PENDING)
                    .createTime(LocalDateTime.now())
                    .build();

                kafkaTemplate.send("seckill-orders", event);

                log.info("秒杀预扣成功: userId={}, activityId={}, remaining={}",
                    userId, activityId, remainingStock);

                return SeckillResult.success("排队中,请稍后查看订单状态");

            } else if ("repeat_purchase".equals(message)) {
                return SeckillResult.fail("您已参与过此活动,每人限购一件");
            } else if ("sold_out".equals(message)) {
                return SeckillResult.fail("抱歉,商品已被抢光");
            } else {
                return SeckillResult.fail("系统繁忙");
            }

        } catch (Exception e) {
            log.error("秒杀预扣异常: userId={}, activityId={}", userId, activityId, e);
            return SeckillResult.fail("系统繁忙,请重试");
        }
    }
}

3.3 异步订单处理

java
@Component
@RequiredArgsConstructor
@Slf4j
public class SeckillOrderConsumer {

    private final OrderService orderService;
    private final InventoryService inventoryService;
    private final NotificationService notificationService;

    /**
     * 消费秒杀订单消息 — 异步创建正式订单
     */
    @KafkaListener(
        topics = "seckill-orders",
        groupId = "seckill-order-consumer",
        concurrency = 10  // 10个并行消费者
    )
    public void handleSeckillOrder(
            @Payload OrderCreatedEvent event,
            Acknowledgment acknowledgment) {

        String traceId = MDC.get("traceId");
        log.info("[{}] 处理秒杀订单: userId={}, activityId={}",
            traceId, event.getUserId(), event.getActivityId());

        try {
            // 1. 创建订单记录
            Order order = orderService.createSeckillOrder(event);

            // 2. 最终扣减库存(DB 层面)
            boolean deductSuccess = inventoryService.confirmDeductStock(
                event.getActivityId(),
                order.getId()
            );

            if (deductSuccess) {
                // 3. 更新订单状态为待支付
                orderService.updateStatus(order.getId(), OrderStatus.PENDING_PAYMENT);

                // 4. 发送支付超时消息(30分钟后自动取消)
                kafkaTemplate.send("payment-timeout",
                    PaymentTimeoutEvent.builder()
                        .orderId(order.getId())
                        .timeoutMinutes(30)
                        .build()
                );

                // 5. 发送通知
                notificationService.sendOrderNotification(order);

            } else {
                // 库存扣减失败 — 取消订单
                log.warn("库存最终扣减失败: orderId={}", order.getId());
                orderService.cancelOrder(order.getId(), "库存不足");
                // 回补 Redis 库存
                restoreStock(event.getActivityId());
            }

            acknowledgment.acknowledge();

        } catch (Exception e) {
            log.error("处理秒杀订单失败", e);
            // 不 ack,触发重试
        }
    }
}

四、防刷与限流策略

4.1 多维限流体系

图表渲染中…

4.2 Nginx 限流配置

nginx
# nginx-seckill.conf — 秒杀专用限流配置

# 定义限流区域(漏桶算法)
# $binary_remote_addr: 按 IP 限制
# rate=10r/s: 每秒允许 10 个请求
# burst=20: 允许突发 20 个请求排队
# nodelay: 不延迟处理(超过直接拒绝)
limit_req_zone $binary_remote_addr zone=per_ip_limit:10m rate=10r/s;
limit_req_zone $server_name zone=global_limit:10m rate=10000r/s;

server {
    listen 80;
    server_name seckill.example.com;

    # 全局限流
    limit_req zone=global_limit burst=50 nodelay;

    location /api/seckill/ {
        # IP 级别限流
        limit_req zone=per_ip_limit burst=5 nodelay;

        # 限流后的返回码(默认 503)
        limit_req_status 429;

        # 代理到后端
        proxy_pass http://seckill-backend:8080;

        # 超时设置
        proxy_connect_timeout 2s;
        proxy_read_timeout 3s;
    }

    # 返回友好的限流提示
    error_page 429 = @too_many_requests;

    location @too_many_requests {
        default_type application/json;
        return 429 '{"code":429,"message":"操作太频繁了,请休息一下再试"}';
    }
}

4.3 人机验证(滑块验证码)

java
/**
 * 滑块验证码服务
 * 在秒杀按钮点击时触发
 */
@Service
@RequiredArgsConstructor
public class CaptchaService {

    private final RedisTemplate<String, String> redisTemplate;

    /**
     * 生成滑块验证码
     */
    public CaptchaDTO generateCaptcha(String sessionId) {
        // 1. 生成随机滑动距离和坐标
        int targetX = ThreadLocalRandom.current().nextInt(100, 250);
        int targetY = ThreadLocalRandom.current().nextInt(20, 80);

        // 2. 生成背景图(带缺口)和滑块图
        CaptchaImage bgImage = generateBackgroundWithGap(targetX, targetY);
        CaptchaImage sliderImage = generateSliderBlock(targetX, targetY);

        // 3. 将答案存入 Redis(有效期 60 秒)
        String captchaKey = "captcha:" + sessionId;
        redisTemplate.opsForValue().set(
            captchaKey,
            targetX + "," + targetY,
            Duration.ofSeconds(60)
        );

        return CaptchaDTO.builder()
            .captchaKey(captchaKey)
            .bgImage(bgImage.toBase64())
            .sliderImage(sliderImage.toBase64())
            .expiresIn(60)
            .build();
    }

    /**
     * 验证滑块结果
     */
    public boolean verifyCaptcha(String captchaKey, int slideDistance) {
        String answer = redisTemplate.opsForValue().getAndDelete(captchaKey);
        if (answer == null) {
            return false;  // 已过期或不存在
        }

        String[] parts = answer.split(",");
        int targetX = Integer.parseInt(parts[0]);
        // 允许 ±5px 的误差
        return Math.abs(slideDistance - targetX) <= 5;
    }
}

五、前端优化策略

5.1 防止重复提交

javascript
// seckill.js — 秒杀页面前端逻辑

class SeckillPage {
    constructor() {
        this.isSubmitting = false;
        this.countdownTimer = null;
        this.activityId = this.getUrlParam('activityId');
        this.init();
    }

    async init() {
        // 1. 启动倒计时
        await this.fetchServerTime();  // 与服务器校准时间
        this.startCountdown();

        // 2. 绑定按钮事件
        const btn = document.getElementById('seckill-btn');
        btn.addEventListener('click', this.handleSeckill.bind(this));
    }

    // 防抖提交 — 防止疯狂点击
    async handleSeckill(e) {
        e.preventDefault();

        if (this.isSubmitting) {
            this.showToast('正在处理中,请勿重复点击...');
            return;
        }

        // 检查倒计时是否结束
        if (!this.isCountdownFinished()) {
            this.showToast('活动尚未开始!');
            return;
        }

        // 先展示滑块验证码
        const captchaPassed = await this.showCaptchaChallenge();
        if (!captchaPassed) {
            this.showToast('验证失败');
            return;
        }

        // 正式提交
        this.isSubmitting = true;
        this.setButtonLoading(true);

        try {
            const response = await fetch(`/api/seckill/${this.activityId}/order`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-User-ID': getCurrentUserId(),
                    'X-Token': getAuthToken(),
                },
            });

            const result = await response.json();

            if (result.success) {
                this.showToast('🎉 抢购成功!正在排队中...', 'success');
                // 轮询订单状态
                this.pollOrderStatus(result.orderId);
            } else {
                this.showToast(result.message || '抢购失败', 'error');
            }
        } catch (error) {
            this.showToast('网络错误,请重试', 'error');
        } finally {
            this.isSubmitting = false;
            this.setButtonLoading(false);
        }
    }

    // 与服务器时间同步(每 5 秒校准一次)
    async fetchServerTime() {
        const resp = await fetch('/api/seckill/time');
        const { serverTime } = await resp.json();
        this.timeOffset = Date.now() - serverTime;
    }

    // 精确倒计时
    startCountdown() {
        this.countdownTimer = setInterval(() => {
            const remaining = this.calculateRemaining();
            this.updateCountdownDisplay(remaining);

            if (remaining <= 0) {
                clearInterval(this.countdownTimer);
                this.enableSeckillButton();
            }
        }, 200);  // 200ms 刷新一次
    }
}

// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
    new SeckillPage();
});

六、性能基准与容量规划

6.1 性能目标

指标目标值说明
峰值 QPS100,000+支撑百万用户同时抢购
P99 延迟< 200ms从用户点击到返回结果
可用性99.99%年停机 < 52 分钟
零超卖100% 保证库存精确
防刷能力> 99.9% 拦截机器人/脚本拦截率

6.2 容量规划

code
┌──────────────────────────────────────────────┐
│         秒杀系统容量规划 (100万用户场景)         │
├──────────────────────────────────────────────┤
│                                              │
│  前端 (CDN):                                 │
│    ├── Cloudflare: 200+ 全球边缘节点          │
│    └── 静态资源带宽: 10 Gbps                  │
│                                              │
│  接入层:                                     │
│    ├── API Gateway: 8 核 16G × 3 节点         │
│    ├── Nginx: 4核 8G × 5 节点               │
│    └── 吞吐量: 15w QPS                       │
│                                              │
│  缓存层:                                     │
│    ├── Redis Cluster: 6 主 6 从              │
│    ├── 内存: 64GB 总量                       │
│    └── QPS: 50w+ (纯内存操作)                 │
│                                              │
│  消息队列:                                   │
│    ├── Kafka: 3 broker × 3 partition          │
│    ├── 吞吐量: 50w msg/s                     │
│    └── 保留时间: 7 天                         │
│                                              │
│  业务层:                                     │
│    ├── 订单服务: 8 pod × 4核 8G             │
│    ├── 库存服务: 4 pod × 4核 8G             │
│    └── 支付服务: 4 pod × 2核 4G             │
│                                              │
│  数据库:                                     │
│    ├── MySQL: 一主两从 (32核 128GB SSD)      │
│    └── 连接池: 每服务最大 50 连接             │
│                                              │
└──────────────────────────────────────────────┘

七、2026 最佳实践总结

7.1 秒杀系统设计原则

  1. 尽量少做事:能过滤的请求绝不放进去
  2. 充分利用缓存:所有读操作走缓存
  3. 异步化一切:非核心流程全部异步
  4. 前端配合:倒计时、防抖、友好提示
  5. 兜底预案:降级、熔断、人工干预

7.2 生产环境 Checklist

  • 全链路压测:至少进行 3 轮压测,覆盖峰值 120%
  • 限流配置到位:CDN→网关→接入→业务 四层限流
  • 缓存预热完成:活动开始前 10 分钟完成数据加载
  • 监控大盘就绪:QPS、延迟、错误率、库存余量实时展示
  • 告警规则配置:关键指标异常立即告警
  • 回滚预案:一键停止秒杀、恢复库存
  • 客服准备:FAQ 文档、话术模板
  • 法务审核:活动规则、退款政策

八、延伸资源

经典案例

开源项目


本文版本:2026 重制版 | 基于本文档第61篇原文重构 最后更新:2026-06-06 | 技术栈:Redis 7.2 / Kafka 3.7 / Spring Boot 3.2 / Nginx 1.25 / Go 1.22